﻿// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace SnapObjects.Data
{
    internal abstract class RoutineParameterSchemaProvider
    {
        protected readonly DataContext _dataContext;

        public RoutineParameterSchemaProvider(DataContext dataContext)
        {
            _dataContext = dataContext;
        }

        protected virtual bool IgnoreUnnamedParameter => true;

        protected virtual bool SkipReturnParameter => false;

        protected abstract IList<DynamicModel> QueryParameters(
            RoutineNameInfo nameInfo, ISqlGenerationHelper helper);

        protected abstract Task<IList<DynamicModel>> QueryParametersAsync(
            RoutineNameInfo nameInfo, ISqlGenerationHelper helper, CancellationToken cancellationToken);

        protected abstract ProcedureSchema CreateParameterSchema(
            string name,
            IReadOnlyList<AdoDbParameter> adoDbParameters,
            IReadOnlyList<DynamicModel> dbParameters);

        public IReadOnlyList<ProcedureSchema> GetParameterSchema(string name)
        {
            var nameInfo = this.ParseName(name);

            var parameters = this.QueryParameters(
                nameInfo, _dataContext.AdoDbSqlProvider.SqlGenerationHelper);

            //There may be overloaded functions
            var procedures = parameters.GroupBy(m => m.GetValue("object_uniqueidentifier"));

            var schemas = new List<ProcedureSchema>();

            foreach (var procedure in procedures)
            {
                var tempParams = procedure.ToList();

                var adoDbParameters = this.ConvertToAdoDbParameter(tempParams);

                var schema = this.CreateParameterSchema(nameInfo.FullName, adoDbParameters, tempParams);

                schemas.Add(schema);
            }

            return schemas;
        }

        public async Task<IReadOnlyList<ProcedureSchema>> GetParameterSchemaAsync(
            string name, CancellationToken cancellationToken)
        {
            var nameInfo = this.ParseName(name);

            var parameters = await this.QueryParametersAsync(
                nameInfo, _dataContext.AdoDbSqlProvider.SqlGenerationHelper, cancellationToken);

            //There may be overloaded functions
            var procedures = parameters.GroupBy(m => m.GetValue("object_uniqueidentifier"));

            var schemas = new List<ProcedureSchema>();

            foreach (var procedure in procedures)
            {
                var tempParams = procedure.ToList();

                var adoDbParameters = this.ConvertToAdoDbParameter(tempParams);

                var schema = this.CreateParameterSchema(nameInfo.FullName, adoDbParameters, tempParams);

                schemas.Add(schema);
            }

            return schemas;
        }

        protected IReadOnlyList<AdoDbParameter> ConvertToAdoDbParameter(IList<DynamicModel> parameters)
        {
            var result = new List<AdoDbParameter>();

            var position = 0;

            foreach (var parameter in parameters)
            {
                var paramName = this.GetName(parameter);

                if (String.IsNullOrWhiteSpace(paramName) && this.IgnoreUnnamedParameter)
                {
                    continue;
                }

                var direction = this.GetDirection(parameter);

                if (this.SkipReturnParameter && direction == ParameterDirection.ReturnValue)
                {
                    continue;
                }

                var scale = this.GetScale(parameter);
                var precision = this.GetPrecision(parameter);

                (var typeName, var mapping) = this.GetParameterType(parameter, precision, scale);

                var hasDefaultValue = this.TryGetDefaultValue(parameter, mapping, out var defaultValue);

                var size = this.GetSize(parameter);

                var adoParameter = new AdoDbParameter(
                    paramName, 
                    defaultValue,
                    hasDefaultValue,
                    mapping?.ClrType ?? typeof(object), 
                    direction, 
                    typeName, 
                    precision, 
                    scale, 
                    position++,
                    size,
                    null);

                if (!result.Any(m => m.ParameterName == adoParameter.ParameterName))
                {
                    result.Add(adoParameter);
                }
            }

            return result;
        }

        protected virtual int GetSize(DynamicModel parameter)
        {
            return 0;
        }

        protected virtual bool TryGetDefaultValue(DynamicModel parameter, TypeMapping mapping, out object value)
        {
            value = parameter.GetValue("parameter_defaultvalue");

            if (value != null && mapping != null)
            {
                try
                {
                    value = mapping.ChangeDbType(value);

                    return true;
                }
                catch
                { 
                    //TODO...
                }
            }

            return false;
        }

        protected virtual (string, TypeMapping) GetParameterType(DynamicModel parameter, byte precision, byte scale)
        {
            var paramType = parameter.GetValue<string>("parameter_datatype");

            var typeMapper = _dataContext.AdoDbSqlProvider.TypeMapper;

            var mapping = typeMapper.FindMapping(paramType);

            return (paramType, mapping);
        }

        protected virtual RoutineNameInfo ParseName(string procedureName)
        {
            var nameInfo = new RoutineNameInfo();

            var name = String.Empty;

            var names = procedureName.Split(new char[] { '.' });

            if (names.Length == 1)
            {
                name = names[0].Trim();
            }
            else if (names.Length == 2)
            {
                name = names[1].Trim();
                nameInfo.Schema = names[0].Trim();
                nameInfo.FullName = nameInfo.Schema;
            }
            else if (names.Length >= 3)
            {
                name = names[names.Length - 1];

                nameInfo.Schema = names[names.Length - 2];
                nameInfo.Database = names[names.Length - 3];
                nameInfo.FullName = $"{nameInfo.Database}.{nameInfo.Schema}";
            }

            var tempNames = name.Split(';');

            if (tempNames.Length > 0)
            {
                nameInfo.Name = tempNames[0].Trim();

                nameInfo.FullName = !String.IsNullOrWhiteSpace(nameInfo.FullName)
                    ? $"{nameInfo.FullName}.{nameInfo.Name}" : nameInfo.Name;

                if (tempNames.Length == 2 && Int32.TryParse(tempNames[1], out var num))
                {
                    nameInfo.Number = num;
                }
            }

            return nameInfo;
        }

        private byte GetPrecision(DynamicModel parameter)
        {
            var parameter_precision = parameter.GetValue("parameter_precision")?.ToString();

            Byte.TryParse(parameter_precision, out var precision);

            return precision;
        }

        private byte GetScale(DynamicModel parameter)
        {
            var parameter_scale = parameter.GetValue("parameter_scale")?.ToString();

            Byte.TryParse(parameter_scale, out var scale);

            return scale;
        }

        private string GetName(DynamicModel parameter)
        {
            var helper = _dataContext.AdoDbSqlProvider.SqlGenerationHelper;

            var paramName = parameter.GetValue<string>("parameter_name");

            if (!String.IsNullOrWhiteSpace(paramName) &&
                !paramName.StartsWith(helper.ParameterIdentifier + ""))
            {
                paramName = helper.GenerateParameterName(paramName);
            }

            return paramName;
        }

        private ParameterDirection GetDirection(DynamicModel parameter)
        {
            var parameterModel = parameter.GetValue<string>("parameter_mode");

            var direction = ParameterDirection.Input;

            if (!String.IsNullOrWhiteSpace(parameterModel))
            {
                switch (parameterModel.ToLower())
                {
                    case "out":
                    case "output":
                    {
                        direction = ParameterDirection.Output;
                        break;
                    }
                    case "inout":
                    case "inputoutput":
                    case "in/out":
                    {
                        direction = ParameterDirection.InputOutput;
                        break;
                    }
                    case "return":
                    case "returnvalue":
                    {
                        direction = ParameterDirection.ReturnValue;
                        break;
                    }
                }
            }

            return direction;
        }
    }
}
